/****************************************************************************************** Import */
import _ from 'lodash';
import Collect from '../helper/colelct';

/****************************************************************************************** Class */
/**
 * 请求设置对象结构：
 * {
 *      url: string,                            // url地址,
 *      data: object,                           // 参数键值对,      
 *      pipe: {                                 // http处理管道（注意：此结构下的所有方法，都可直接在根下使用（如果根下有同名方法，则会覆盖pipe中的方法））
 *          start() {},                         // 开始请求时执行的方法
 *          end() {},                           // 请求完成时执行的方法
 *          success(data, obj) {},              // 成功时执行的方法
 *          error(res, obj) {},                 // 失败时执行的方法（专指逻辑失败）
 *          lose(text, obj) {},                 // 接口丢失（通常为404错误）时执行的方法
 *          crash(text, obj) {},                // 接口崩溃（通常为50X错误）时执行的方法
 *          chaos(text, obj) {},                // 格式不正确（返回值混乱，不符合格式预期）时执行的方法
 *          offline(err) {}                     // 没有联网时执行的方法
 *      }
 * }
 */
export default class Http {

    /************************************************************************************** 外部调用接口 */
    /**
     * get请求，参数请看_request方法
     * 
     * @param {*} obj 访问请求设置对象 
     */
    static get(obj) {
        obj = this._setObj(obj);
        this.request(Object.assign(obj, {method: 'GET'}));
    }

    /**
     * post请求，参数请看_request方法
     * 
     * @param {*} obj 访问请求设置对象 
     */
    static post(obj) {
        obj = this._setObj(obj);
        this.request(Object.assign(obj, {method: 'POST'}));
    }

    /************************************************************************************** 真实请求方法 */
    /**
     * 真实访问方法
     * 
     * @param {*} method 访问方法
     * @param {*} obj 访问请求设置对象 
     */
    static request(obj) {

        // ------------------------------------------------------ 
        // 执行开始回调
        this._start(obj);

        // ------------------------------------------------------ 
        // 根据obj设置reqConfig。reqConfig是真正需要带入request的配置
        let reqConfig = { method: obj.method };
  
        // ------------------------------------------------------ 
        // 设置url（主要用于get）
        if (obj.method == 'GET') {
            obj.url = obj.url + this._paramGet(obj.url, obj.data);
        }
        
        // ------------------------------------------------------ 
        // 设置body（主要用于post）
        // 注：如果不需要添加body字段，则Param.post函数返回的是null
        if (obj.method == 'POST') {
            let result = this._paramPost(obj.data);
            if (result != null) {
                reqConfig['body'] = result;
            }
        }

        // ------------------------------------------------------ 
        // 设置reqConfig的header参数
        reqConfig['headers'] = this.getHeaders();

        // ------------------------------------------------------ 
        // 发起请求，并返回数据
        // 判断是否存在服务器状态码错误，最后将文本类型的返回值交给下一步
        fetch(obj.url, reqConfig).then((res)=> {

            // 在debug模式下，输出response信息
            console.log('--------- ' + obj.method + ' : ' + obj.url);
            if (_.isObject(obj.data)) {
                console.log(obj.data);
            }
            console.log(res);
            console.log('--------- END');

            // 返回码判断，如果不等于200，则：收集错误，调用错误回调，并直接返回
            const statusCode = parseInt(res.status);
            if (statusCode != 200) {

                // 如果status == 404，则记录lose错误
                if (statusCode >= 400 && statusCode < 500) {
                    Collect.lose(res, obj);
                    if (_.isFunction(obj.pipe.lose)) {
                        obj.pipe.lose(res, obj);
                    }
                }

                // 如果status == 50x，则记录crash错误
                if (statusCode >= 500) {
                    Collect.crash(res, obj);
                    if (_.isFunction(obj.pipe.crash)) {
                        obj.pipe.crash(res, obj);
                    }
                }

                // 只要出错，均返回null
                return null;
            }

            // 将数据返回给下一步，注意，这里全部返回为text类型
            return res.text(); 
        })

        // ------------------------------------------------------ 
        // 数据处理
        .then((text)=> {

            // 执行结束回调
            this._end(obj);

            // 空值判断，返回码不为200时，text为null，则直接返回
            if (text == null) {
                return;
            }
            
            // 赋值parse类型
            this.parse(text, obj);
        })

        // ------------------------------------------------------ 
        // 错误抓取
        .catch((err) => {

            // 一但抓取到错误，立即报出，用于调试（正式环境中不会报错）
            console.error(err);

            // 执行结束回调
            this._end(obj);

            // 收集错误，并调用错误回调
            Collect.offline(err, obj);
            if (_.isFunction(obj.pipe.offline)) {
                obj.pipe.offline(err, obj);
            }
        });
    }

    /************************************************************************************** 子类继承重写方法 */
    /**
     * 注意：获取header信息（子类需要重写）
     */
    static getHeaders() {}

    /**
     * 注意：解析返回的字符串（子类需要重写）
     * 
     * @param {String} text 从服务器返回的原始字符串
     * @param {Object} obj 设置对象
     */
    static parse(text, obj) {}

    /************************************************************************************** 检查传递的参数是否在config中存在 */
    /**
     * 检查obj参数，并使用obj根下的pipe函数，覆盖掉obj.pipe下的pipe函数
     * 
     * @param {Object} obj http请求设置对象
     */
    static _setObj(obj) {

        // 检查http请求对象中的：url, pipe是否全部存在
        if (!_.has(obj, 'url') || !_.has(obj, 'pipe')) {
            console.error('http请求对象参数obj，至少包含：url, pipe两个参数！');
            console.log('当前obj对象：');
            console.log(obj);
        }

        // obj根下管道函数，覆盖obj.pipe管道函数
        const tmp = _.omit(obj, ['url', 'data', 'pipe']);
        Object.assign(obj.pipe, tmp);
        obj = _.omit(obj, ['start', 'end', 'success', 'error', 'lose', 'crash', 'chaos', 'offline']);

        // 返回
        return obj;
    }

    /************************************************************************************** 参数处理方法 */
    /**
     * 设置Get方法的参数
     * 
     * @param {String} url 此次访问的url
     * @param {Object} data 数据对象
     * @return {String} 需要连接到url之后的querystring
     */
    static _paramGet(url, data) {

        // 如果不是平面对象，则直接返回空串
        if (!_.isPlainObject(data)) {
            return '';
        }

        // 计算params数组
        let params = [];
        for (let key in data) {
            params.push(key + '=' + data[key]);
        }

        // 计算得到连接符号和查询字符串
        const symbol = url.indexOf('?') === -1 ? '?' : '&';
        const query = params.join('&');

        // 返回连接在url之后需要连接的参数字符串
        return symbol + query;
    }

    /**
     * 设置Post方法的参数
     * 
     * @param {Object} data 数据对象
     * @return {FormData} 如果存在data，则返回新的formData数据，否则返回null
     */
    static _paramPost(data) {

        // 如果不是平面对象，则直接返回false（代表不需要在obj添加body）
        if (!_.isPlainObject(data)) {
            return null;
        }

        // 制作formData对象，并返回
        let formData = new FormData();
        for (let key in data) {
            formData.append(key, data[key])
        }

        // 返回formData数据（代表需要在obj添加body）
        return formData;
    }

    /************************************************************************************** 指示器方法 */
    /**
     * 如果带有指示器，则显示指示器
     * 注意start和end都必须是方法
     * 
     * @param {Object} obj 设置对象
     */
    static _start(obj) {
        if (_.isFunction(obj.pipe.start)) {
            obj.pipe.start(obj);
        }
    }

    /**
     * 如果带有指示器，则隐藏指示器
     * 注意start和end都必须是方法
     * 
     * @param {Object} obj 设置对象
     */
    static _end(obj) {
        if (_.isFunction(obj.pipe.end)) {
            obj.pipe.end(obj);
        }
    }
}
